This is the minimalist’s missing Haskell tutorial, intended to bridge the gap between
language-focused tutorials and setting up a project.
Getting started with the REPL
To get started as quick as possible, let’s fire up the Glasgow Haskell Compiler’s (GHC) Read Eval Print Loop (REPL), the GHCi, where the ‘i’ stands for interpreter. Feed it code and it will be interpreted, not compiled. It will be slower, but you get quick feedback during development.
$ ghci
GHCi, version 8.8.4: https://www.haskell.org/ghc/ :? for help
Prelude> 2 + 2
4
Inside the REPL, one can define and use functions
Prelude> times2 x = x + x
Prelude> times2 2
4
This quickly becomes awkward with longer function bodies. In principle, one can
use multiline definitions
Prelude> :{
Prelude| times2 x =
Prelude| x + x
Prelude| :}
Prelude> times2 2
4
but this usually isn’t super practical either. If there is a typo, one cannot easily re-create
the whole thing. Pressing the Up-Arrow key gives us the previously entered commands from the command history.
But here it gives us one line at a time, so we would need to put the pieces back together one by one.
So the obvious thing is to define programs in files.
Writing programs
We can put some code in a file and run with the interpreter.
main = do
print "Hello, World!"
If the runhaskell is installed on your machine, you can run it with
examples/haskell$ runhaskell hello.hs
"Hello, World!"
Note: Haskell files should be formatted using spaces for the indentation. Also note that the indentation carries meaning, that is, the compiler will complain and not compile incorrectly formatted files.
Now we run this code from within the interpreter
examples/haskell$ ghci hello.hs
GHCi, version 8.8.4: https://www.haskell.org/ghc/ :? for help
[1 of 1] Compiling Main ( hello.hs, interpreted )
Ok, one module loaded.
*Main> main
"Hello, World!"
To reload after changes have been made (remove the ! for example) use :r.
*Main> :r
[1 of 1] Compiling Main ( hello.hs, interpreted )
Ok, one module loaded.
*Main> main
"Hello, World"
You can define functions
examples/haskell/functions.hs:
times2 n = n + n
main = do
print (times2 2)
That means one can develop can call different parts indepently inside the interpeter
examples/haskell$ ghci functions.hs
GHCi, version 8.8.4: https://www.haskell.org/ghc/ :? for help
[1 of 1] Compiling Main ( functions.hs, interpreted )
Ok, one module loaded.
*Main> main
4
*Main> times2 2
4
Now our problem from earlier is solved. We can confortably use a text editor to edit our
functions instead of doing that on the REPL, but we can trigger them from the REPL and see their effects.
Modules
Programs are distributed into modules, which sit in different files.
examples/haskell/modules1/MyModule.hs:
module MyModule where
times2 n = n + n
examples/haskell/modules1/go.hs:
import MyModule
main = do
print (times2 2)
Run this with
examples/haskell$ cd modules1
examples/haskell/modules1$ ghci go.hs
GHCi, version 8.8.4: https://www.haskell.org/ghc/ :? for help
[1 of 1] Compiling Main ( go.hs, interpreted )
Ok, one module loaded.
*Main> main
4
*Main> times2 2
4
Note that :r after making changes to any module leads to the entire thing to be re-interpreted.
So a new call to main should reflect any changes.
Note also that since MyModule is imported into the Main module, we can call times2 from within the
*Main module. Actually what the *Main> prompt tells us is that we operate inside the context of the Main module.
Contrast that with when you open the interpreter without arguments ($ ghci). This will show you the Prelude> prompt,
signalling we operate withing the context of the standard library (Prelude).
If (and only if) you make a change to MyModule.hs and afterwards call :r MyModule, the prompt will change to *MyModule>, so we operate in the context of the module MyModule and will be able only to access things defined there. Entering :r will bring
us back.
Submodules
With more code the next question to address is how to organise it hierarchically.
The following should serve as an example.
examples/haskell/submodules/MyModule/MySubModule.hs:
module MyModule.MySubModule where
times3 x = x + x + x
examples/haskell/submodules/MyModule.hs:
module MyModule where
import MyModule.MySubModule
times2 x = x + x
times6 x = times3 x + times3 x
examples/haskell/submodules/main.hs
import MyModule
import MyModule.MySubModule
main = do
print (times2 2)
print (times3 3)
print (times6 6)
Run it
examples/haskell/submodules$ runhaskell main.hs
4
9
36
Qualified imports
When importing modules, both the module’s own and the imported modules’
symbols become available, which can lead to clashes. One means of avoiding
it is qualifying imports, such that imported functions are called with a prefix of choice.
We assume the same folder layout as in the previous example
and change only main.hs.
examples/haskell/qualified_imports/main.hs:
import MyModule as MM
import MyModule.MySubModule as MSM
main = do
print (MM.times2 2)
print (MSM.times3 3)
print (MM.times6 6)
In the REPL
$ ghci main
...
*Main> main
4
9
36
*Main> MM.times2 2
4
One also could use M.MySubModule, that is, something including dots, as the shorthand.
Making a Main module
Given
examples/haskell/main_module/MyModule.hs
module MyModule where
times2 x = x + x
when we create a Main module
examples/haskell/main_module/Main.hs
module Main where
import MyModule as MM
main = do
print (MM.times2 2)
instead of giving main.hs as parameter, we use Main, like this
examples/haskell/main_module$ runhaskell Main
or like this
examples/haskell/main_module$ ghci Main
Using available modules
A lot of modules come already with a Haskell installation. I am not sure
which ones these are, but Data.Char is amongst them and should serve as our example here.
examples/haskell/external_modules/main.hs
import Data.Char
main = do
print $ show $ isLower 'a'
Run it
examples/haskell/external_modules$ runhaskell main.hs
"True"
Installing external modules with Cabal
Modules come bundled in packages and are available via the Hackage package registry.
I am sure they can be downloaded and linked with more low level tools, but nowadays those things are handled by a package manager,
in our case Cabal.
Let us try out downloading a package and make use of a new module. We start by updating the package info with
$ cabal update
Then we download the package ABList
$ cabal install --lib ABList
This installs and makes the package globally available. When you fire up ghci, you should see something like this
$ ghci
GHCi, version 8.10.5: https://www.haskell.org/ghc/ :? for help
Loaded package environment from /<userdir>/.ghc/x86_64-darwin-8.10.5/environments/default
Prelude>
We can list available symbols with
Prelude> :browse Data.AbList
type Data.AbList.AbList :: * -> * -> *
data Data.AbList.AbList a b
= Data.AbList.AbNil | a Data.AbList.:/ (Data.AbList.AbList b a)
Data.AbList.aaFromList :: [a] -> Data.AbList.AbList a a
Data.AbList.aaMap ::
(a -> b) -> Data.AbList.AbList a a -> Data.AbList.AbList b b
Data.AbList.aaToList :: Data.AbList.AbList a a -> [a]
Data.AbList.abFoldl ::
(t -> Either a b -> t) -> t -> Data.AbList.AbList a b -> t
...
and use them
Prelude> import Data.AbList
Prelude Data.AbList> abFromPairs [(1,"3"),(3,"7")]
1 :/ ("3" :/ (3 :/ ("7" :/ AbNil)))
Don’t ask :D. I have no idea what it does.
Anyways, like you did earlier with import Data.Char, in every program (i.e. not only in ghci) you can
import modules from the ABList package.
Looking at the output of cat ~/.ghc/x86_64-darwin-8.10.5/environments/default (see path given above when running ghci),
we should see the ABList package listed.
...
package-id ghc-8.10.5
package-id bytestring-0.10.12.0
package-id unix-2.7.2.2
package-id base-4.14.2.0
package-id time-1.9.3
...
package-id text-1.2.4.1
package-id ABLst-0.0.3-261a7b41 <--------------- here
Note in case at some point you run into trouble with unsatisfiable dependencies, i.e. when you cabal install out
put includes something like this
Resolving dependencies...
cabal: Could not resolve dependencies:
rejecting: ...
the easy (brute force) way out is
$ rm -rf ~/.ghc ~/.cabal
This resets all packages. Next time you want to install something, chances are good that it works, but it affects all your
other projects. All dependencies need to be downloaded anew. Make sure to follow this up with a cabal update, otherwise it will complain
that it cannot find packages.
Concluding this secontion, we should say that instead of installing packages globally,
it is probably better to set up projects with Cabal and declaring package dependencies ‘locally’.
Using external modules with Cabal projects
Cabal is not only a package-, but also a build-manager, which can help building and packaging more complex projects.
Let’s set up an example project using it.
examples/haskell$ mkdir cmodules && cd cmodules # already done in
examples/haskell/cmodules$ cabal init # example project
We also want to demonstrate how to use external dependencies. For that, add a dependency to
the package split by editing
examples/haskell/cmodules/cmodules.cabal
...
executable cmodules
...
build-depends: base >=4.12 && <=4.18.0
, split >=0.2.3.5 && <=0.2.3.5
...
examples/haskell/cmodules/Main.hs:
module Main where
import Data.List
import Data.List.Split
main :: IO ()
main = putStrLn $ concat (splitOn "x" "axbxc")
Run
examples/haskell/cmodules$ cabal run cmodules
Resolving dependencies...
... lots of text
Building executable 'cmodules' for cmodules-0.1.0.0..
... more text
Linking ...
abc
Testing with HUnit
Now let’s set up some unit tests.
$ mkdir hunit_testing
$ cd hunit_testing
$ cabal init
$ rm -r app
<pre hidden> TODO Actually it would be nicer to not repeat the info in the above block since we already explained that </pre>
examples/haskell/hunit_testing/hunit-testing.cabal:
...
test-suite my-test-suite
type: exitcode-stdio-1.0
main-is: MyTestSuite.hs
build-depends: base >=4.13 && <4.18
, HUnit
default-language: Haskell2010
...
examples/haskell/hunit_testing/MyTestSuite.hs:
module Main where
import Test.HUnit
test1 = TestCase $ assertEqual "a succeeding test" "a" "a"
test2 = TestCase $ assertEqual "a failing test" "a" "b"
main =
runTestTT $ TestList [test1, test2]
Note that we need to name the module Main despite it having a different file name.
We can run it either with
examples/haskell/hunit_testing$ cabal run my-test-suite
...
### Failure in: 1
MyTestSuite.hs:7
a failing test
expected: "a"
but got: "b"
Cases: 2 Tried: 2 Errors: 0 Failures: 1
or we can use
examples/haskell/hunit_testing$ cabal test my-test-suite
...
Test suite my-test-suite: RUNNING...
Test suite my-test-suite: PASS
Test suite logged to:
<...>my-test-suite/test/hunit-testing-0.1.0.0-my-test-suite.log
1 of 1 test suites (1 of 1 test cases) passed.
the difference being that in the latter case we need to look up the test results in the log file
$ cat <...>my-test-suite/test/hunit-testing-0.1.0.0-my-test-suite.log
...
Test suite my-test-suite: RUNNING...
### Failure in: 1
MyTestSuite.hs:7
a failing test
expected: "a"
but got: "b"
Cases: 2 Tried: 2 Errors: 0 Failures: 1
Test suite my-test-suite: PASS
...
I’m not sure why this is considered PASSed, but whatever 🤷; at least it correctly counts 1 Failure.
Separate src and test folders
examples/haskell/separate_src_and_test/separate-src-and-test.cabal:
...
executable main
main-is: Main.hs
hs-source-dirs: src
build-depends: base >=4.13 && <=4.18
default-language: Haskell2010
other-modules: MyModule
test-suite my-test-suite
type: exitcode-stdio-1.0
main-is: MyTestSuite.hs
hs-source-dirs: src, test
build-depends: base >=4.13 && <=4.18
, HUnit
default-language: Haskell2010
other-modules: MyModule
...
examples/haskell/separate_src_and_test/src/Main.hs:
module Main where
import MyModule
main =
print $ times2 2
examples/haskell/separate_src_and_test/src/MyModule.hs:
module MyModule where
times2 x = x + x
examples/haskell/separate_src_and_test/test/MyTestSuite.hs:
module Main where
import Test.HUnit
import MyModule
test1 = TestCase $ assertEqual "a failing test" 5 (times2 2)
main =
runTestTT $ TestList [test1]
Run test
examples/haskell/separate_src_and_test$ cabal run my-test-suite
...
### Failure in: 0
test/MyTestSuite.hs:6
a failing test
expected: 5
but got: 4
Cases: 1 Tried: 1 Errors: 0 Failures: 1
Debugging
Many people find is perfectly fine to do the good old printf-debugging.
To do that in haskell, we can make use of the Debug.Trace module, which ships
with the base package.
It comes with a couple of handy functions, of of which is trace. With trace you
can do something like this:
examples/haskell/debugging/dbg.hs:
import Debug.Trace
debug msg d = trace (msg ++ show d) d
times2 = (*) 2
main =
putStrLn
$ show
$ debug "2: "
$ times2
$ debug "1: "
$ times2
$ 3
We wrap trace in a convenient helper function which can be inserted into a flow.
Run it
examples/haskell/debugging$ runhaskell dbg.hs
1: 6
2: 12
12
It prints intermediate values but otherwise proceeds
to transform values as if the debug calls weren’t there.
You can conveniently comment out and comment in the debug lines during debugging.
One more example shows how we can define debug slightly differently to fit another use case:
examples/haskell/debugging/dbg2.hs:
import Debug.Trace
debug d msg = trace (msg ++ show d) d
isPos n
| n<0 = False `debug` "f: "
| otherwise = True `debug` "t: "
main =
putStrLn
$ show
$ isPos 3
Run it
examples/haskell/debugging$ runhaskell dbg2.hs
t: True
True
Defining it thus allows us again to comment out the debug calls.
isPos n
| n<0 = False -- `debug` "f: "
| otherwise = True -- `debug` "t: "
Final words
If you liked this and want to suggest an improvement,
feel free to shoot me a mail.